﻿namespace Chameleon.Data
{
	using Chameleon.Collections;
	using System;
	using System.Collections;
	using System.Collections.Generic;
	using System.Linq;

	/// <summary>
	/// Provides a volatile in-memory imitation of a persistence context for data-bound objects.
	/// <para>This type of depository is intended to be used predominantly for unit testing of database operations.</para>
	/// </summary>
	
	internal sealed class MimicDepository : Depository
	{
		#region Fields

			/// <summary>
			/// Collection of items considered to be committed. 
			/// </summary>
			
			IList<object> Committed = new List<object>();


			/// <summary>
			/// Collection of items considered to be awaiting insertion on commit.
			/// </summary>

			IList<object> PendingInsert = new List<object>();


			/// <summary>
			/// Collection of items considered to be awaiting delete on commit.
			/// </summary>

			IList<object> PendingDelete = new List<object>();


			/// <summary>
			/// Collection of item being tracked by the current instance.
			/// </summary>

			IList<object> Tracked = new List<object>();

		#endregion


		#region IDepository

			/// <summary>
			/// Returns an interface for querying items of a specified type within the data context.
			/// </summary>
			/// <typeparam name="TEntity">Type of the items to query.</typeparam>
			
			protected internal override IQueryable<TEntity> Query<TEntity>()
			{
				var query = Committed
					.OfType<TEntity>()
					.AsQueryable();

				// set up interception for the query enumeration
				var intercepted = new InterceptedQuery<TEntity>( query );
				intercepted.Enumerated += WhenEnumerated;

				return intercepted;
			}


			/// <summary>
			/// Inserts an item into the underlying data context and returns a reference to the item.
			/// </summary>
			/// <typeparam name="TEntity">Type of the item to insert into the data context.</typeparam>
			/// <param name="item">Item to insert into the data context.</param>

			public override TEntity Insert<TEntity>( TEntity item )
			{
				// if the item already exists in either collection, throw an exception
				if ( Committed.Contains( item ) || PendingInsert.Contains( item ) )
					throw new ArgumentException( "Item already exists." );

				// add item to pending queue
				Track( item );
				PendingInsert.Add( item );
				return item;
			}


			/// <summary>
			/// Attaches a previously disconnected item to the context.
			/// </summary>
			/// <typeparam name="TEntity">Type of the item to attach to the context.</typeparam>
			/// <param name="item">Item to attach to the data context.</param>

			public override void Attach<TEntity>( TEntity item )
			{
				if ( !Committed.Contains( item ) )
					throw new ArgumentException( "Item does not exist." );

				if ( Tracked.Contains( item ) )
					throw new ArgumentException( "Item is already being tracked." );

				Track( item );
			}


			/// <summary>
			/// Deletes an item from the underlying data context.
			/// <para>This will not take effect until a commit is performed.</para>
			/// </summary>
			/// <typeparam name="TEntity">Type of the item to delete from the data context.</typeparam>
			/// <param name="item">Item to be removed from the data context.</param>

			public override void Delete<TEntity>( TEntity item )
			{
				if ( !Tracked.Contains( item ) )
					throw new ArgumentException( "Cannot delete items that aren't managed by the context." );

				if ( !Committed.Contains( item ) )
					throw new ArgumentException( "Item doesn't exist." );

				if ( PendingDelete.Contains( item ) )
					throw new ArgumentException( "Item is already pending deletion." );

				PendingDelete.Add( item );
			}


			/// <summary>
			/// Commits any outstanding changes to the underlying data context.
			/// </summary>
			/// <remarks>
			/// Note that deleted items are not removed from tracking - this is to help detect a logic error where
			/// an item previously deleted is attached.
			/// </remarks>

			public override void Submit()
			{
				foreach ( var i in PendingInsert )
					Committed.Add( i );

				foreach ( var i in PendingDelete )
					Committed.Remove( i );
					
				PendingInsert.Clear();
				PendingDelete.Clear();
			}

		#endregion


		#region IDisposable

			/// <summary>
			/// Performs tasks associated with freeing, releasing, or resetting unmanaged resources.
			/// </summary>
			/// <param name="disposing">When true, releases unmanaged resources being used by managed components.</param>
			
			protected override void Dispose( bool disposing ) {}

		#endregion


		#region Methods

			/// <summary>
			/// Adds the item to the collection of tracked items if it isn't already being tracked.
			/// </summary>
			/// <param name="item">Item to track.</param>
			
			void Track( object item )
			{
				lock ( Tracked )
				{
					if ( !Tracked.Contains( item ) )
						Tracked.Add( item );
				}
			}


			/// <summary>
			/// Handles notifications that items were enumerated from a query.
			/// </summary>
			/// <param name="sender">Object that raised the event.</param>
			/// <param name="arguments">Event arguments including the object that was enumerated.</param>
			
			void WhenEnumerated( object sender, InterceptedEnumerationEventArgs arguments )
			{
				Track( arguments.Item );
			}

		#endregion


		#region Constructors

			/// <summary>
			/// Initializes a new empty in-memory depository.
			/// </summary>
			
			internal MimicDepository() {}


			/// <summary>
			/// Initializes a new in-memory depository with a specified collection of items to be considered pre-committed.
			/// </summary>
			/// <param name="existing">Collection of items to be considered already committed.</param>
			/// <remarks>
			/// This constructor is intended for testing-use only.
			/// Items in the collection will not go through the insert logic.
			/// </remarks>

			internal MimicDepository( IEnumerable existing )
			{
				foreach ( var e in existing )
					Committed.Add( e );
			}

		#endregion
	}
}
